In [2]:
# Simplest way to create a Python object
class Polynomial1:
pass
p1 = Polynomial1()
p2 = Polynomial1()
p1.coeffs = 1,2,3
p2.coeffs = 3,4,5
# The problem with this code is that it contains repeated code
In [29]:
class Polynomial2:
def __init__(self, *coeffs):
self.coeffs = coeffs
p1 = Polynomial2(1,2,3)
p2 = Polynomial2(3,4,5)
# Now we accomplished the same task of object creation by using init function
In [30]:
print(p1)
print(p2)
# Now this looks so ugly
In [31]:
# I am missing __repr__ in my class
class Polynomial3:
def __init__(self, *coeffs):
self.coeffs = coeffs
def __repr__(self):
return 'Polynomial(*{!r})'.format(self.coeffs)
p1 = Polynomial3(1,2,3)
p2 = Polynomial3(3,4,5)
print(p1)
print(p2)
# The function implemented in __repr__ by uses !r whcih is used to place the contenst of format
In [34]:
# Now I want to add them
class Polynomial4:
def __init__(self, *coeffs):
self.coeffs = coeffs
def __repr__(self):
return 'Polynomial(*{!r})'.format(self.coeffs)
def __add__(self, other):
return Polynomial4(*(x+y for x, y in zip(self.coeffs, other.coeffs)))
def __len__(self):
return len(self.coeffs)
p1 = Polynomial4(1,2,3)
p2 = Polynomial4(3,4,5)
print(p1+p2)
print(len(p1))
In [35]:
# The double underscore functions that I use here are called data model methods
# x + y -> __add__
# init x -> __init__
# represent x -> __repr__
# We can see that there is strong correlation ampong the function implemented by data model functions and the name of the function
In [36]:
# To understand the object oriented view of python you must know these three
# 1) The protocols aka data model
# 2) Built in inheritance protocol
# 3) How oop in python works
In [50]:
# For this example suppose you have two teams develoer and core infrastructure team that writes library software. The library provides classes that are than made
# subclass in user
In [ ]:
# Case 1 where you work at user.py
In [46]:
# Library.py
class Base:
def foo(self):
return 'foo'
In [49]:
# user.py
assert hasattr(Base, 'foo'), "You broke it fool!" # It checks that the base library contains the needed methods
class Derived(Base):
def bar(self):
return self.foo
# This statement can break when their is no foo method in the base class. To overcome this you can use assert
In [51]:
# Case 2 where you work at library.py
# Here you assume that you will implement bar function in the future and you want to deal with the user.py for not to use that method
In [1]:
# library.py
# We can use try except but that will only work at run time
class Base:
def foo(self):
return self.bar
In [2]:
# user.py
class Derived(Base):
def bar(self):
return 'bar'
In [3]:
# To view actual runtime instructions
from dis import dis
def _():
class Some:
pass
dis(_)
In [4]:
# What it does is it shows the exact instructions that python follows at runtime
# Now we have a underscore function that allows you to view your class while you are building it
In [6]:
old_bc = __build_class__
def my_bc(*a, **kw):
print('my build class -> ', a, kw)
return old_bc(*a, **kw)
import builtins
builtins.__build_class__ = my_bc
print(my_bc)
In [6]:
# Now I can use the obove approach to check classes for functions
old_bc = __build_class__
def my_bc(fun, name, base=None, **kw):
if base == Base:
print('Check if bar function is defined')
if base is not None:
return old_bc(fun, name, bases, **kw)
return old_bc(fun, name, **kw)
import builtins
builtins.__build_class__ = my_bc
In [7]:
# The above method works when you run the code from a terminal and in practice it is not implemented in this way.
# To solve our case 2 problem we use metaclasses.
In [1]:
# library.py
class BaseType(type):
def __new__(cls, name, bases, body):
print('BaseMeta.__new__', cls, name, bases, body)
return super().__new__(cls, name, bases, body)
class Base(metaclass = BaseType):
def foo(self):
return self.bar()
In [ ]:
# We can see the body of the class as a dict.
# Now to get the desired behaviour e can use assert
In [4]:
class BaseType(type):
def __new__(cls, name, bases, body):
if not 'bar' in body:
raise TypeError ("Bad user class")
return super().__new__(cls, name, bases, body)
class Base(metaclass = BaseType):
def foo(self):
return self.bar()
In [6]:
# We get the error message as we have not yet made the bar method in Base
In [9]:
# Suppose I want to time the given function \
def add(x, y=10):
return x+y
print('add(10) ', add(10))
print('add(20,30) ', add(20,30))
print('add("a", "b") ', add("a", "b"))
In [12]:
from time import time
def add(x, y=10):
return x+y
before = time()
print('add(10) ', add(10))
after = time()
print('Time taken ', after - before)
before = time()
print('add(20,30) ', add(20,30))
after = time()
print('Time taken ', after - before)
before = time()
print('add("a", "b") ', add("a", "b"))
after = time()
print('Time taken ', after - before)
# This approach uis bad as code repetition
In [15]:
def add(x, y=10):
before = time()
rv = x + y
after = time()
print('Time taken ', after - before)
return rv
print('add(10) ', add(10))
print('add(20,30) ', add(20,30))
print('add("a", "b") ', add("a", "b"))
# Now if I have another function like
def sub(x, y=10):
return x-y
# Then you would have to enter all the code again and it will become a mess for a large number of functions
In [19]:
def timer(func, x, y=10):
before = time()
rv = func(x,y)
after = time()
print('Time taken ', after-before)
return rv
def add(x, y=10):
return x+y
def sub(x, y=10):
return x-y
print('add(10) ', timer(add, 10))
print('add(20,30) ', timer(add, 20,30))
print('add("a", "b") ', timer(add, "a", "b"))
# This approach is better than the last one
In [26]:
def timer(func):
def f(x,y=10):
before = time()
rv = func(x,y)
after = time()
print('Time taken ', after-before)
return rv
return f
def add(x, y=10):
return x+y
add = timer(add)
def sub(x, y=10):
return x-y
sub = timer(sub)
print('add(10) ', add(10))
print('add(20,30) ', add(20,30))
print('add("a", "b") ', add("a", "b"))
# Here I wrapped a function around another function
In [27]:
# The above functionality of wrapping a function around another function is provided by decorator
def timer(func):
def f(x,y=10):
before = time()
rv = func(x,y)
after = time()
print('Time taken ', after-before)
return rv
return f
@timer
def add(x, y=10):
return x+y
@timer
def sub(x, y=10):
return x-y
print('add(10) ', add(10))
print('add(20,30) ', add(20,30))
print('add("a", "b") ', add("a", "b"))
In [29]:
# If you want to run a function n times say decorators
def ntimes(n):
def inner(f):
def wrapper(*args, **kwargs):
for _ in range(n):
print('running {.__name__}'.format(f))
rv = f(*args, **kwargs)
return rv
return wrapper
return inner
@ntimes(2)
def add(x, y=10):
return x+y
@ntimes(5)
def sub(x, y=10):
return x-y
print('add(10)', add(10))
print('add(10,20)', add(10,20))
print('sub(10)', sub(10))
print('sub(20,10)', sub(20,10))
In [32]:
# Every x() function is implemented by __call__
def add1(x,y):
return x+y
class Adder:
def __call__(self, x, y):
return x+y
add2 = Adder()
print(add1(10,20))
print(add2(10,20))
# The only difference between them is that add1 is easy to write than add2. Internally add1 is implemented in a very close manner as add2
In [34]:
# Suppose you are doing database loading with python and doing some operation which I show for simplicity as waiting got .5 sec.
from time import sleep
def compute():
rv = []
for i in range(10):
sleep(.5)
rv.append(i)
return rv
compute()
# The problem with this function is that it returns all the values together. Suppose that the wait time is very large than you will
# have to wait a long time. Or you could get one value at a time and process it one by one.
Out[34]:
In [37]:
class Compute:
def __iter__(self):
self.last = 0
return self
def __next__(self):
rv = self.last
self.last += 1
if(self.last > 10):
raise StopIteration()
sleep(.5)
return rv
for val in Compute():
print(val)
# This method also uses less memory as earlier we had to store the entire list and here it is not the case
In [40]:
# Generator syntax allows you to write the __iter__ and __call__ in a nice and clean manner
def compute():
for i in range(10):
sleep(.5)
yield i
for val in compute():
print(val)
In [42]:
class API:
def run_this_first(self):
first()
def run_this_second(self):
second()
def run_this_last(self):
last()
# Now to make sure that they run in the same order you can use generator. Creating a new function with all the mehtods written in the
# desired order is not an option like
# def doit():
# first()
# second()
# last()
# because we want to get user input adter each function completes it's execution
def api():
first()
yield
second()
yield
last()
# Here after running first() it will wait for user response and then go to second()
In [43]:
# Similar to resource allocation is initialization. Suppose you open a file and then after you operate on it, you would have to close in
# order to free up the memory or flush it out.
In [ ]: